﻿// Copyright (c) Microsoft Open Technologies, Inc.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Xml.Schema;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis
{
    /// <summary>
    /// This type is responsible for parsing a ruleset xml file and producing a <see cref="RuleSet"/> object.
    /// </summary>
    internal class RuleSetProcessor
    {
        // Strings for the RuleSet node
        private const string RuleSetNodeName = "RuleSet";
        private const string RuleSetNameAttributeName = "Name";
        private const string RuleSetDescriptionAttributeName = "Description";
        private const string RuleSetToolsVersionAttributeName = "ToolsVersion";

        // Strings for the Rules node
        private const string RulesNodeName = "Rules";
        private const string RulesAnalyzerIdAttributeName = "AnalyzerId";
        private const string RulesNamespaceAttributeName = "RuleNamespace";

        // Strings for the Rule node
        private const string RuleNodeName = "Rule";
        private const string RuleIdAttributeName = "Id";

        // Strings for the Include node
        private const string IncludeNodeName = "Include";
        private const string IncludePathAttributeName = "Path";

        // Strings for the IncludeAll node
        private const string IncludeAllNodeName = "IncludeAll";

        // Strings for the Action attribute and its values
        private const string RuleActionAttributeName = "Action";
        private const string RuleActionNoneValue = "None";
        private const string RuleActionHiddenValue = "Hidden";
        private const string RuleActionInfoValue = "Info";
        private const string RuleActionWarningValue = "Warning";
        private const string RuleActionErrorValue = "Error";
        private const string RuleActionDefaultValue = "Default";

        private const string XsdResourceName = "Microsoft.CodeAnalysis.RuleSet.xsd";

        /// <summary>
        /// Static constructor for initializing the schema object to be used for schema validation
        /// </summary>
        private static XmlSchema CreateRuleSetSchema()
        {
            Stream xsdStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(XsdResourceName);
            XmlSchema xmlSchema = XmlSchema.Read(xsdStream, null);
            return xmlSchema;
        }

        /// <summary>
        /// Creates and loads the rule set from a file
        /// </summary>
        /// <param name="filePath">The file path to load the rule set</param>
        public static RuleSet LoadFromFile(string filePath)
        {
            // First read the file without doing any validation
            filePath = FileUtilities.NormalizeAbsolutePath(filePath);
            XmlReaderSettings settings = GetDefaultXmlReaderSettings();

            XmlDocument ruleSetDocument = new XmlDocument();
            XmlNode ruleSetNode = null;

            using (FileStream stream = FileUtilities.OpenRead(filePath))
            using (XmlReader xmlReader = XmlReader.Create(stream, settings))
            {
                try
                {
                    ruleSetDocument.Load(xmlReader);
                }
                catch (XmlException e)
                {
                    throw new InvalidRuleSetException(e.Message);
                }

                // Find the top level rule set node
                XmlNodeList nodeList = ruleSetDocument.GetElementsByTagName(RuleSetNodeName);
                if (nodeList.Count != 1)
                {
                    Debug.Fail("Multiple top-level nodes!");
                }
                Debug.Assert(nodeList[0].Name == RuleSetNodeName);
                ruleSetNode = nodeList[0];
            }

            // Validate against the current schema
            var ruleSetSchema = CreateRuleSetSchema();
            ruleSetDocument.Schemas.Add(ruleSetSchema);
            ruleSetDocument.Schemas.Compile();
            try
            {
                ruleSetDocument.Validate(null);
            }
            catch (XmlSchemaValidationException e)
            {
                throw new InvalidRuleSetException(string.Format(CodeAnalysisResources.RuleSetSchemaViolation, e.Message));
            }

            return ReadRuleSet(ruleSetNode, filePath);
        }

        /// <summary>
        /// Load the rule set from the XML node
        /// </summary>
        /// <param name="ruleSetNode">The rule set node from which to create a rule set object</param>
        /// <param name="filePath">The file path to the rule set file</param>
        /// <returns>A rule set object with data from the given XML node</returns>
        private static RuleSet ReadRuleSet(XmlNode ruleSetNode, string filePath)
        {
            var specificOptions = new Dictionary<string, ReportDiagnostic>();
            var generalOption = ReportDiagnostic.Default;
            var includes = new List<RuleSetInclude>();

            // Loop through each rules or include node in this rule set
            foreach (XmlNode childNode in ruleSetNode.ChildNodes)
            {
                if (childNode.Name == RulesNodeName)
                {
                    var rules = ReadRules(childNode);
                    foreach (var rule in rules)
                    {
                        var ruleId = rule.Key;
                        var action = rule.Value;

                        ReportDiagnostic existingAction;
                        if (specificOptions.TryGetValue(ruleId, out existingAction))
                        {
                            if (existingAction != action)
                            {
                                throw new InvalidRuleSetException(string.Format(CodeAnalysisResources.RuleSetHasDuplicateRules, ruleId, existingAction, action));
                            }
                        }
                        else
                        {
                            specificOptions.Add(ruleId, action);
                        }
                    }
                }
                else if (childNode.Name == IncludeNodeName)
                {
                    includes.Add(ReadRuleSetInclude(childNode));
                }
                else if (childNode.Name == IncludeAllNodeName)
                {
                    generalOption = ReadIncludeAll(childNode);
                }
                else
                {
                    // we are not interested in this node.
                    continue;
                }
            }

            return new RuleSet(filePath, generalOption, specificOptions, includes);
        }

        /// <summary>
        /// Load the rules from the XML node
        /// </summary>
        /// <param name="rulesNode">The rules node from which to loop through each child rule node</param>
        /// <returns>A list of rule objects with data from the given XML node</returns>
        private static List<KeyValuePair<string, ReportDiagnostic>> ReadRules(XmlNode rulesNode)
        {
            string analyzerId = ReadNonEmptyAttribute(rulesNode, RulesAnalyzerIdAttributeName);
            string ruleNamespace = ReadNonEmptyAttribute(rulesNode, RulesNamespaceAttributeName);

            var rules = new List<KeyValuePair<string, ReportDiagnostic>>();

            // Loop through each rule node
            foreach (XmlNode ruleNode in rulesNode.ChildNodes)
            {
                if (ruleNode.Name == RuleNodeName)
                {
                    rules.Add(ReadRule(ruleNode, analyzerId, ruleNamespace));
                }
                else
                {
                    // Schema validation should prevent us from getting here
                    Debug.Fail("Unknown child node in Rules node");
                }
            }

            return rules;
        }

        /// <summary>
        /// Load the rule from the XML node
        /// </summary>
        /// <param name="ruleNode">The rule node from which to create a rule object</param>
        /// <param name="analyzer">The analyzer this rule belongs to</param>
        /// <param name="space">The namespace this rule belongs to</param>
        /// <returns>A rule object with data from the given XML node</returns>
        private static KeyValuePair<string, ReportDiagnostic> ReadRule(XmlNode ruleNode, string analyzer, string space)
        {
            string ruleId = ReadNonEmptyAttribute(ruleNode, RuleIdAttributeName);
            ReportDiagnostic action = ReadAction(ruleNode);

            return new KeyValuePair<string, ReportDiagnostic>(ruleId, action);
        }

        /// <summary>
        /// Load the included rule set from the XML node
        /// </summary>
        /// <param name="includeNode">The include node from which to create a RuleSetInclude object</param>
        /// <returns>A RuleSetInclude object with data from the given XML node</returns>
        private static RuleSetInclude ReadRuleSetInclude(XmlNode includeNode)
        {
            string includePath = ReadNonEmptyAttribute(includeNode, IncludePathAttributeName);
            ReportDiagnostic action = ReadAction(includeNode);

            return new RuleSetInclude(includePath, action);
        }

        /// <summary>
        /// Reads the action from the given node
        /// </summary>
        /// <param name="node">The node to read the action, it can be a rule node or an include node</param>
        /// <returns>The rule action</returns>
        private static ReportDiagnostic ReadAction(XmlNode node)
        {
            string action = ReadNonEmptyAttribute(node, RuleActionAttributeName);

            if (string.Equals(action, RuleActionWarningValue))
            {
                return ReportDiagnostic.Warn;
            }
            else if (string.Equals(action, RuleActionErrorValue))
            {
                return ReportDiagnostic.Error;
            }
            else if (string.Equals(action, RuleActionInfoValue))
            {
                return ReportDiagnostic.Info;
            }
            else if (string.Equals(action, RuleActionHiddenValue))
            {
                return ReportDiagnostic.Hidden;
            }
            else if (string.Equals(action, RuleActionNoneValue))
            {
                return ReportDiagnostic.Suppress;
            }
            else if (string.Equals(action, RuleActionDefaultValue))
            {
                return ReportDiagnostic.Default;
            }

            // Schema validation should prevent us from getting here.
            Debug.Fail("Invalid action value in RuleSet");
            throw ExceptionUtilities.Unreachable;
        }

        /// <summary>
        /// Load the IncludedAll from the XML node
        /// </summary>
        /// <param name="includeAllNode">The IncludeAll node from which to create a IncludeAll object</param>
        /// <returns>A IncludeAll object with data from the given XML node</returns>
        private static ReportDiagnostic ReadIncludeAll(XmlNode includeAllNode)
        {
            return ReadAction(includeAllNode);
        }

        /// <summary>
        /// Reads an attribute from a node and validates that it is not empty.
        /// </summary>
        /// <param name="node">The XML node that contains the attribute</param>
        /// <param name="attributeName">The name of the attribute to read</param>
        /// <returns>The attribute value</returns>
        private static string ReadNonEmptyAttribute(XmlNode node, string attributeName)
        {
            XmlAttribute attribute = node.Attributes[attributeName];
            if (attribute == null || string.IsNullOrEmpty(attribute.Value))
            {
                Debug.Fail("Failed to validate attribute '" + attributeName + "' in node '" + node.Name + "'");
            }

            return attribute.Value;
        }

        /// <summary>
        /// Gets the default settings to read the rulset xml file.
        /// </summary>
        private static XmlReaderSettings GetDefaultXmlReaderSettings()
        {
            XmlReaderSettings xmlReaderSettings = new XmlReaderSettings();

            xmlReaderSettings.CheckCharacters = true;
            xmlReaderSettings.CloseInput = true;
            xmlReaderSettings.ConformanceLevel = ConformanceLevel.Document;
            xmlReaderSettings.IgnoreComments = true;
            xmlReaderSettings.IgnoreProcessingInstructions = true;
            xmlReaderSettings.ValidationType = ValidationType.None;
            xmlReaderSettings.IgnoreWhitespace = true;
            xmlReaderSettings.DtdProcessing = DtdProcessing.Prohibit;
            xmlReaderSettings.XmlResolver = null;

            return xmlReaderSettings;
        }
    }
}
